using System;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using msilgc.common;

namespace msilgc.driver.cuda.Compilers
{
	internal class MicrosoftCpp : INativeCompiler
	{
		public MicrosoftCpp ()
		{
		}

		public void RunCompiler(DirectoryInfo dllOutputPath, IEnumerable<FileInfo> kernelFiles, out string compilerMsgs) {
			compilerMsgs = "";
			foreach (var kernelFile in kernelFiles) {
				string msg;
				RunCompiler(dllOutputPath, kernelFile, out msg);
				compilerMsgs += Environment.NewLine + msg;
			}
		}

		private static void RunCompiler(DirectoryInfo dllOutputPath, FileInfo kernelFile, out string compilerMsgs) {
			Logger.WriteLine("\tC++ Compiling...");
			using (var nvcc = new Process()) {
				nvcc.StartInfo.FileName = Path.Combine(CudaSettings.CudaBinPath, "nvcc.exe");
				nvcc.StartInfo.Arguments = string.Format(
					"-ccbin \"{0}\" -c -Dinline=__inline -DWIN64 -D_MBCS -Xcompiler /EHsc,/W3,/nologo,/Wp64,/O2,/Zi,/MT -I\"{1}\" -I\"{2}\" -o \"{3}\" \"{4}\"",
					MsDotNetSettings.MicrosoftCPPPath,
					CudaSettings.CudaIncludePath,
					MsDotNetSettings.MicrosoftIncPath,
					Path.Combine(dllOutputPath.FullName, kernelFile.Name.Replace(".cu", ".o")),
					Path.Combine(dllOutputPath.FullName, kernelFile.Name));
				nvcc.StartInfo.RedirectStandardError = true;
				nvcc.StartInfo.RedirectStandardOutput = true;
				nvcc.StartInfo.UseShellExecute = false;
				nvcc.StartInfo.CreateNoWindow = true;

				if (!nvcc.Start()) {
					throw new Exception("compiler was not started");
				}

				using (var reader = new ProcessOutputReader(nvcc)) {
					if (!nvcc.WaitForExit(30000)) {
						nvcc.Kill();
						throw new Exception("compiler didn't exit within 30 seconds");
					}
					compilerMsgs = reader.GetOutput();
				}
				if (!File.Exists(Path.Combine(dllOutputPath.FullName, kernelFile.Name.Replace(".cu", ".o"))) || (nvcc.ExitCode != 0)) {
					throw new Exception(
						"failed to compile.\r\ncompiler output: " + compilerMsgs +
						"\r\ncompiler command line: " + nvcc.StartInfo.Arguments);
				}
			}
		}

		public void RunLinker(DirectoryInfo dllOutputPath, IEnumerable<string> objectFileBaseNames) {
			Logger.WriteLine("\tC++ Linking...");
			using (var linker = new Process()) {
				linker.StartInfo.FileName = Path.Combine(MsDotNetSettings.MicrosoftCPPPath, "link.exe");
				const string standardLibraries =
					"kernel32.lib user32.lib gdi32.lib comdlg32.lib advapi32.lib shell32.lib cudart.lib";
				const string linkerOptions =
					"/INCREMENTAL:NO /NOLOGO /DLL /MANIFEST:NO /SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /DYNAMICBASE /NXCOMPAT /MACHINE:X64 /ERRORREPORT:PROMPT";
				string fileList = objectFileBaseNames.Select(fileName => Path.Combine(dllOutputPath.FullName, fileName + ".o")).Aggregate((x, y) => x + " " + y);

				var validLibPaths = string.Join(" ", 
				                                from path in new[] { CudaSettings.CudaLibraryPath, MsDotNetSettings.WindowsSdkFolder, Path.Combine(MsDotNetSettings.VisualCppInstallFolder, "lib\\amd64") }
				where Directory.Exists(path)
				let padded = Pad(path)
				let arg = string.Format("/LIBPATH:{0}", padded)
				select arg);

				linker.StartInfo.Arguments = string.Format(
					"/OUT:\"{0}\" " + linkerOptions + " {1} " + standardLibraries + " cudart.lib \"{2}\"",
					Path.Combine(dllOutputPath.FullName, "driver.dll"),
					validLibPaths, fileList);

				var envPath = GetPath();

				linker.StartInfo.EnvironmentVariables["PATH"] = envPath;

				linker.StartInfo.RedirectStandardError = true;
				linker.StartInfo.RedirectStandardOutput = true;
				linker.StartInfo.UseShellExecute = false;
				linker.StartInfo.CreateNoWindow = true;
				if (!linker.Start()) {
					throw new Exception("linker was not started");
				}
				using (var reader = new ProcessOutputReader(linker)) {
					linker.WaitForExit();
					if (!File.Exists(Path.Combine(dllOutputPath.FullName, "driver.dll"))) {
						var output = reader.GetOutput();
						throw new Exception(
							"failed to link.\r\n" +
							"return code: " + linker.ExitCode + "\r\n" +
							"linker output: " + output + "\r\n" +
							"linker command line: " + linker.StartInfo.Arguments + "\r\n\r\n" +
							"PATH: " + envPath);
					}
				}
			}
		}

		private static string GetPath() {
			var newPaths = new[]
			{
				Path.Combine(MsDotNetSettings.VisualCppInstallFolder, "BIN\\amd64"),
				@"C:\Windows\Microsoft.NET\Framework64\v3.5",
				@"C:\Windows\Microsoft.NET\Framework64\v2.0.50727",
				Path.Combine(MsDotNetSettings.VisualCppInstallFolder, "VCPackages"),
				Path.Combine(MsDotNetSettings.VisualStudioInstallFolder, "Common7\\IDE"),
				Path.Combine(MsDotNetSettings.VisualStudioInstallFolder, "Common7\\Tools"),
				Path.Combine(MsDotNetSettings.VisualStudioInstallFolder, "Common7\\Tools\\bin"),
			};
			var currentPath = Environment.GetEnvironmentVariable("PATH");
			var current = currentPath == null ? new string[0] : currentPath.Split(Path.PathSeparator);

			var newPathSet = newPaths.Concat(current).Distinct();

			var newPath =
				(from path in newPathSet
				 where Directory.Exists(path)
				 let padded = Pad(path)
				 select padded).Aggregate((x, y) => string.Concat(x, Path.PathSeparator, y));

			return newPath;
		}

		private static string Pad(string path) {
			const string quote = "\"";
			if (path.Length > 3 && path.EndsWith(Path.DirectorySeparatorChar.ToString())) {
				path = path.Substring(0, path.Length - 1);
			}
			if (path.Contains(" ")) {
				if (!(path.StartsWith("\"") && path.StartsWith("\""))) {
					return string.Concat(quote, path, quote);
				} else if (!path.StartsWith("\"")) {
					return string.Concat(quote, path);
				} else if (!path.EndsWith("\"")) {
					return string.Concat(path, quote);
				}
			}
			return path;
		}
	}
}

